package cn.xeblog.plugin.game.game2048;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 2048游戏界面
 */
public class GameUI extends JPanel {
    /**
     * 默认背景色
     */
    private static final Color BG_COLOR = new Color(0xbbada0);
    /**
     * 字体名称
     */
    private static final String FONT_NAME = "";
    /**
     * 行列数
     */
    private static final int LINE = 4;
    /**
     * 每个方块的大小
     */
    private static final int BLOCK_SIZE = 48;
    /**
     * 每个方块的间距
     */
    private static final int BLOCK_MARGIN = 10;
    /**
     * 方块2生成的概率,剩下为方块4的概率
     */
    private static final double PROBABILITY = 0.75;
    /**
     * 方块集合
     */
    private Block[] blocks;
    /**
     * 是否赢家
     */
    private boolean isWin = false;
    /**
     * 是否输家
     */
    private boolean isLose = false;
    /**
     * 分数
     */
    private int score = 0;

    /**
     * 游戏界面
     */
    public GameUI() {
        // 设置此组件的首选大小
        setPreferredSize(new Dimension(200, 200));
        setFocusable(true);
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                // 游戏结束 且 空格
                if ((isWin || isLose) && e.getKeyCode() == KeyEvent.VK_SPACE) {
                    // 重置游戏
                    resetGame();
                }

                // 如果不能移动，说明输了
                if (!canMove()) {
                    isLose = true;
                }

                // 游戏进行中，没有赢也没有输
                if (!isWin && !isLose) {
                    // 监听方向键
                    switch (e.getKeyCode()) {
                        case KeyEvent.VK_LEFT:
                            // 左
                            left();
                            break;
                        case KeyEvent.VK_RIGHT:
                            // 右
                            right();
                            break;
                        case KeyEvent.VK_DOWN:
                            // 下
                            down();
                            break;
                        case KeyEvent.VK_UP:
                            // 上
                            up();
                            break;
                    }
                }
                // 如果没赢，并且无法移动，标记为输
                if (!isWin && !canMove()) {
                    isLose = true;
                }
                // 刷新界面
                repaint();
            }
        });
        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                // 聚焦
                requestFocusInWindow();
            }
        });
        // 重置游戏，即初始化游戏属性
        resetGame();
    }

    /**
     * 重置游戏，即初始化游戏属性
     */
    public void resetGame() {
        score = 0;
        isWin = false;
        isLose = false;
        blocks = new Block[LINE * LINE];
        for (int i = 0; i < blocks.length; i++) {
            blocks[i] = new Block();
        }
        // 刚开始游戏时初始构建两个方块
        addBlock();
        addBlock();
    }

    /**
     * 向左移动
     */
    public void left() {
        // 添加方块标识
        boolean addBlock = false;
        // 遍历行
        for (int i = 0; i < LINE; i++) {
            // 获取指定行
            Block[] line = getLine(i);
            // 合并后的方块组
            Block[] merged = mergeLine(moveLine(line));
            // 设置行数据
            setLine(i, merged);
            // 没有添加方块，且 旧行与合并后的行不相等，那么说明当前游戏未暂停，则将添加方块标识标记为可以添加方块
            if (!addBlock && !compare(line, merged)) {
                addBlock = true;
            }
        }
        // 添加方块
        if (addBlock) {
            addBlock();
        }
    }

    /**
     * 向右移动
     */
    public void right() {
        // 旋转180度，向右移动变成了向左移动
        blocks = rotate(180);
        // 向左移动
        left();
        // 旋转180度，回到原点
        blocks = rotate(180);
    }

    /**
     * 向上移动
     */
    public void up() {
        // 旋转270度，向上移动变成了向左移动
        blocks = rotate(270);
        left();
        // 旋转90度，回到原点
        blocks = rotate(90);
    }

    /**
     * 向下移动
     */
    public void down() {
        //  旋转90度，向下移动变成了向左移动
        blocks = rotate(90);
        // 向左移动
        left();
        // 旋转270度，回到原点
        blocks = rotate(270);
    }

    /**
     * 获取指定下标的方块
     *
     * @param x 横坐标（0~3）
     * @param y 纵坐标（0~3）
     * @return 指定下标的方块
     */
    private Block blockAt(int x, int y) {
        return blocks[x + y * LINE];
    }

    /**
     * 随机添加一个方块
     */
    private void addBlock() {
        List<Block> list = availableSpace();
        if (!list.isEmpty()) {
            // 随机下标
            int index = (int) (Math.random() * list.size()) % list.size();
            // 空的方块
            Block emptyTime = list.get(index);
            // 随机数值
            emptyTime.value = Math.random() < PROBABILITY ? 2 : 4;
        }
    }

    /**
     * 寻找空白方块集合
     *
     * @return 空白方块列表
     */
    private List<Block> availableSpace() {
        return Arrays.stream(this.blocks).filter(Block::isEmpty).collect(Collectors.toCollection(() -> new ArrayList<>(16)));
    }

    /**
     * 是否铺满
     *
     * @return 如果存在空白方块，说明未铺满，返回false,否则,false
     */
    private boolean isFull() {
        return availableSpace().isEmpty();
    }

    /**
     * 是否可以移动
     *
     * @return true，有相同方块，可以移动，否则，false
     */
    boolean canMove() {
        // 未满，可以移动
        if (!isFull()) {
            return true;
        }
        // 如果满了
        for (int x = 0; x < LINE; x++) {
            for (int y = 0; y < LINE; y++) {
                // 指定下标的方块
                Block t = blockAt(x, y);
                // 横坐标必须小于3，从左至右的方向，横坐标的右边与左边相同 或者 纵坐标小于3，从上至下的方向，纵坐标，同一列中，当前列行与下一列的指定下标是否相同
                if ((x < 3 && t.value == blockAt(x + 1, y).value) || ((y < 3) && t.value == blockAt(x, y + 1).value)) {
                    // 如果相同，可以移动
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 方块比较
     *
     * @param line1 方块组1
     * @param line2 方块组2
     * @return 如果相等，返回true,否则，false
     */
    private boolean compare(Block[] line1, Block[] line2) {
        // 方块对象相等
        if (line1 == line2) {
            return true;
        } else if (line1.length != line2.length) {
            // 长度不相等直接返回
            return false;
        }

        // 比较两个方块组同一下标是否相等
        return IntStream.range(0, line1.length).noneMatch(i -> line1[i].value != line2[i].value);
    }

    /**
     * 旋转
     * <a href="https://segmentfault.com/a/1190000042457609">公式参考</a><br>
     *
     * @param angle 旋转的角度
     * @return 旋转后的方块数组
     */
    private Block[] rotate(int angle) {
        // 新的块数组
        Block[] newBlocks = new Block[LINE * LINE];
        // 横轴偏移量
        int offsetX = 3;
        int offsetY = 3;// 纵轴偏移量
        // 如果角度为90时，纵轴偏移量为0
        if (angle == 90) {
            offsetY = 0;
        } else if (angle == 270) {
            // 如果角度为270时，横轴偏移量为0
            offsetX = 0;
        }
        // 弧度,角度转弧度
        double rad = Math.toRadians(angle);
        // 余弦
        int cos = (int) Math.cos(rad);
        // 正弦值
        int sin = (int) Math.sin(rad);

        // 遍历插入
        for (int x = 0; x < LINE; x++) {
            for (int y = 0; y < LINE; y++) {
                // 旋转后的横轴坐标
                int newX = (x * cos) - (y * sin) + offsetX;
                // 旋转后的纵轴坐标
                int newY = (x * sin) + (y * cos) + offsetY;
                // 添加到新的块数组中
                newBlocks[(newX) + (newY) * LINE] = blockAt(x, y);
            }
        }
        return newBlocks;
    }

    /**
     * 移动行
     *
     * @param oldLine 旧的行
     * @return 移动后的行方块数组
     */
    private Block[] moveLine(Block[] oldLine) {
        LinkedList<Block> l = new LinkedList<>();
        // 行数据不空就添加，这样能把空格子排除掉，即[0,2,2,0] -> [2,2]
        for (int i = 0; i < LINE; i++) {
            if (!oldLine[i].isEmpty()) {
                l.addLast(oldLine[i]);
            }
        }

        // 如果行数据为空，就直接返回
        if (l.size() == 0) {
            return oldLine;
        } else {
            // 新的行
            Block[] newLine = new Block[LINE];
            // 确保大小,补充方块，即[0,2,2,0] -> [2,2] -> [2,2,0,0]
            ensureSize(l, LINE);
            // 从头至尾输入到新行
            for (int i = 0; i < LINE; i++) {
                newLine[i] = l.removeFirst();
            }
            return newLine;
        }
    }

    /**
     * 合并行
     *
     * @param oldLine 旧的行数据
     * @return 合并后的行数据
     */
    private Block[] mergeLine(Block[] oldLine) {
        // [0,2,2,0] -> [2,2] -> [2,2,0,0]
        LinkedList<Block> list = new LinkedList<>();

        // 如果当前行为空，则跳出
        for (int i = 0; i < LINE && !oldLine[i].isEmpty(); i++) {
            // 当前的数值
            int num = oldLine[i].value;
            // 当前和后一个比较值是否相等
            if (i < 3 && oldLine[i].value == oldLine[i + 1].value) {
                // 数值*2
                num <<= 1;

                // 分数等于合成的后的数值
                score += num;
                // 如果合成后目录值为2048，那么将为赢家
                int ourTarget = 2048;
                if (num == ourTarget) {
                    isWin = true;
                }
                // 合并后跳过1个格子
                i++;
            }
            // 加入行列表
            list.add(new Block(num));
        }

        // 行列表为空，直接返回旧的行
        if (list.isEmpty()) {
            return oldLine;
        } else {
            // 填充空方块
            ensureSize(list, LINE);
            // 转成数组
            return list.toArray(new Block[LINE]);
        }
    }

    /**
     * 确保尺寸，添加空的方块直到方块列表满足尺寸
     *
     * @param l 方块列表
     * @param s 方块列表的大小
     */
    private static void ensureSize(List<Block> l, int s) {
        // 如果方块尺寸不满足，那么将添加空的方块直到满足尺寸
        while (l.size() != s) {
            l.add(new Block());
        }
    }

    /**
     * 获取一行方块
     *
     * @param index 行下标
     * @return 指定行的方块组
     */
    private Block[] getLine(int index) {
        return IntStream.range(0, LINE).mapToObj(i -> blockAt(i, index)).toArray(Block[]::new);
    }

    /**
     * 设置行
     *
     * @param index 行下标
     * @param re    待设置的行数据
     */
    private void setLine(int index, Block[] re) {
        // 将re数组从下标0开始复制到blocks数组中下标为index * LINE的位置起开始复制长度为Line条数据
        System.arraycopy(re, 0, blocks, index * LINE, LINE);
    }

    @Override
    public void paint(Graphics g) { // 绘制
        super.paint(g);
        // 设置背景色
        g.setColor(BG_COLOR);

        // 填充矩形
        g.fillRect(0, 0, this.getSize().width - 10, this.getSize().height);
        for (int y = 0; y < LINE; y++) {
            for (int x = 0; x < LINE; x++) {
                // 绘制方块
                drawBlock(g, blocks[x + y * LINE], x, y);
            }
        }
    }

    /**
     * 绘制方块、得分、游戏结束
     *
     * @param g2    绘图对象
     * @param block 方块
     * @param x     横坐标
     * @param y     纵坐标
     */
    private void drawBlock(Graphics g2, Block block, int x, int y) {
        Graphics2D g = (Graphics2D) g2;

        // 画线平滑
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);

        // 值
        int value = block.value;
        // 方格的x轴起始点
        int xOffset = offsetCoors(x);
        //  方格的y轴起始点
        int yOffset = offsetCoors(y);
        // 设置背景颜色
        g.setColor(block.getBackground());

        // 填充圆角矩形
        g.fillRoundRect(xOffset, yOffset, BLOCK_SIZE, BLOCK_SIZE, 14, 14);
        // 设置前景色
        g.setColor(block.getForeground());

        // 设置加粗字体
        final int size = value < 100 ? 32 : value < 1000 ? 26 : 18;
        final Font font = new Font(FONT_NAME, Font.BOLD, size);
        g.setFont(font);

        // 方格的字符串
        String s = String.valueOf(value);
        final FontMetrics fm = super.getFontMetrics(font);
        // 字体的宽度
        final int w = fm.stringWidth(s);
        // 字体的高度
        final int h = -(int) fm.getLineMetrics(s, g).getBaselineOffsets()[2];

        // 将值画出来
        if (value != 0) {
            g.drawString(s, xOffset + (BLOCK_SIZE - w) / 2, yOffset + BLOCK_SIZE - (BLOCK_SIZE - h) / 2 - 2);
        }

        // 游戏结束
        if (isWin || isLose) {
            // 对整个画面白色且30透明度罩住
            g.setColor(new Color(255, 255, 255, 30));
            g.fillRect(0, 0, getWidth() - 10, getHeight());

            // 设置字体颜色
            g.setColor(new Color(78, 139, 202));
            g.setFont(new Font(FONT_NAME, Font.BOLD, 36));
            if (isWin) {
                g.drawString("通关成功!", 50, 120);
            }
            if (isLose) {
                g.drawString("游戏结束!", 50, 120);
            }
            // 以棕色设置字体颜色
            if (isWin || isLose) {
                g.setFont(new Font(FONT_NAME, Font.BOLD, 16));
                g.setColor(new Color(0x776e65));
                g.drawString("请按空格重新开始游戏", 40, getHeight() - 60);
            }
        }
        // 以棕色设置字体颜色设置分数
        g.setColor(new Color(0x776e65));
        g.setFont(new Font(FONT_NAME, Font.PLAIN, 18));
        g.drawString("Score: " + score, getWidth() - 140, getHeight() - 10);
    }

    /**
     * 协调偏移
     *
     * @param arg 方块的x,y轴坐标
     * @return 计算后的x或y坐标轴的起点值
     */
    private static int offsetCoors(int arg) {
        // | 间隔+方格长度+间隔+方格长度+间隔+方格长度+间隔+方格长度+间隔 |即方格个数*（方格长度+间隔）+间隔
        return arg * (BLOCK_MARGIN + BLOCK_SIZE) + BLOCK_MARGIN;
    }
}
